/*
 * Copyright (C) 2014 Nathan Templon
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package data;

import static util.ArrayUtil.concat;
import static util.ArrayUtil.toArray;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.swing.JOptionPane;

import log.Logger;

/**
 * A class representing data nodes for ObjectDB
 * @author Nathan Templon
 */
public class DataNode implements ValueNode {
    
    
    /**
     * The constructor for a DataNode object.
     * @param data A string array containing the data to be put into the node, including the title line and brackets, formated by the standards for ObjectDBs.
     */
    public DataNode( String[] data ) {
        this.data = data;
        
        // Create empty lists for everything
        this.nodes = new ArrayList<>();
        
        // sort through the data
        sortData();
    }
    
    
    /**
     * A method called on initialization that sorts through the provided data.
     */
    private void sortData() {
        
        // First, check to see if there are any nested nodes inside of this one
        
        boolean nestedNodes;
        int countOfBars = 0;
        
        // Since title lines are in the form of 'ClassName|ObjectName', searching for '|' will find title lines
        for (String line : data) {
            if (line.contains("|")) countOfBars++;
        }
        
        // One '|' will be present for the title line of the largest-scale node, but a second indicates a nested node.
        nestedNodes = (countOfBars > 1);
        
        // Now, begin to process the data 
        int index = 0; // Stores the index of the current line being looked at.
        int bracketNest = 0; // Stores how deep in the "bracket nest" we are, with 0 being the root level.
        int indexOfStart = 0; // Stores the index in data[] at which the current nested node starts.
        int indexOfFinish = 0; // Stores the index in data[] at which the current nested node ends.
        boolean procNode = false;  // Stores whether or not we are currently processing a node.
        boolean nodeIsArray = false; // Stores wheter or not the currently processed node is an array node.
        
        // Iterate through the lines of data to sort it
        for (String line : data) {
            
            // If we have a blank line, skip it
            if(line.equals("")) {
                index++; // But don't forget to increment the index!
                continue;
            }
            
            // The cases for when the line isn't blank
            
            // A '|' denotes a title line, either for this node or a nested node.
            if (line.contains("|")) {
                
                // If there are nested nodes, indicate that this is where the node starts (is a title line)
                if (bracketNest == 0) {
                    getNamesFrom(line); // parses the line for the object's getName and class getName
                }
                
                // If we are not currently processing a node and we found a line with a '|' that wasn't the title line
                //    of the large-scale node, 
                else if (nestedNodes && !procNode) {
                    indexOfStart = index;
                    procNode = true;
                    
                    if(line.contains("[]")) {
                        nodeIsArray = true;
                    }
                }
            }
            
            // If we have an open bracket line, indicate that we have moved one level deeper into the nest
            else if (line.equals("{")) {
                bracketNest++;
            }
            // If we have a closed bracket line, indicate that we have moved one level out of the nest
            else if (line.equals("}")) {
                bracketNest--;
            
                
                // If, when we move out of one of the nested nodes (as indicated by a close bracket), and bracketNest is 1,
                //    indicating that we are inside of the outermost node, we should create a DataNode object to represent the
                //    nested node that we just moved out of
                if (bracketNest == 1) {
                    indexOfFinish = index;
                    procNode = false; // Noting that we are no longer processing this node
                    
                    if (nodeIsArray) {
                        logger.log("Creating new ArrayNode with title line \"" + data[indexOfStart] + "\".");
                        ArrayNode node = new ArrayNode(Arrays.copyOfRange(data, indexOfStart, indexOfFinish + 1));
                        nodes.add(node);
                    }
                    else {
                        logger.log("Creating new DataNode with title line \"" + data[indexOfStart] + "\".");
                        
                        // Create a new node and add it to the list of nested nodes
                        DataNode node = new DataNode(Arrays.copyOfRange(data, indexOfStart, indexOfFinish + 1));
                        nodes.add(node);
                    }
                    
                    nodeIsArray = false;
                }
            }
            
            // This detects primitive data storage lines of the form TYPE:NAME = VALUE inside of the outermost node (bracketNest == 1)
            if (bracketNest == 1 && line.contains("=") && line.contains(":")) {
                // Separate the value from the header
                int indexOfEquals = line.indexOf("=");
                String header = line.substring(0, indexOfEquals).trim();
                String value = line.substring(indexOfEquals + 1).trim();
                
                // Separate the class getName from the object getName
                int indexOfColon = line.indexOf(":");
                String classString = header.substring(0, indexOfColon).trim();
                String key = header.substring(indexOfColon + 1).trim();
                
                // If the value is equal to [REFERENCE], then it stores a reference to another object in the database, not a literal reference.
                if (value.charAt(0) == '[' && value.charAt(value.length() - 1) == ']') {
                    logger.log("Putting \"" + key + "\" as \"" + classString + "\" to \"" + value + "\" in node \"" + name + "\" as a ReferenceNode.");
                    nodes.add(new ReferenceNode(classString, value));
                }
                // If not, the line stored a primitive value (including a String)
                else {
                    // Log it and add it to our list of values
                    logger.log("Putting \"" + key + "\" as \"" + classString + "\" to \"" + value + "\" in node \"" + name + "\" as a PrimitiveValueNode.");
                    nodes.add(new PrimitiveNode(classString, value));
                }
            }
            
            // Increment the index
            index++;
        }
        
        logger.log("Completed creating DataNode \"" + name + ".\"");
    }
    
    
    /**
     * An internal method that parses a provided title line for an object name and class name.
     * @param line The line provided of the form CLASSNAME|OBJECTNAME
     */
    private void getNamesFrom(String line) {
        
        // Find the '|' and use it to split the line in two
        int index = line.indexOf("|");
        className = line.substring(0, index).trim();
        name = line.substring(index + 1).trim();
    }
    
    
    /**
     * A method for debugging only, which pops up the value of all of the data stored in the node.
     */
    public void display() {
        JOptionPane.showMessageDialog(null, data);
    }
    
    
    /**
     * A method for getting the name of the Object represented by this node.
     * @return Returns the name of the Object represented by this node as a String.
     */
    public String getName() {
        return name;
    }
    
    
    /**
     * A method for getting the name of the Class of the Object represented by this node.
     * @return Returns the name of the Class of the Object represented by this node as a String.
     */
    public String className() {
        return className;
    }
    
    
    /**
     * A method for determining if the data node references other Objects in the database.
     * @return Returns true if the node references other Objects, and false if it does not.
     */
    public boolean hasDBRefs() {
        
        // If this nodes fields explicitly reference another Object, or if any of the
        //   children of this node reference anothe robjec, then this object has other references.
        for (ValueNode node : nodes) {
            if (node instanceof ReferenceNode) {
                return true;
            }
            else if (node instanceof DataNode) {
                if ( ((DataNode)node).hasDBRefs() ) return true;
            }
        }
        
        // If neither this or any of this node's children reference a node, then this node does not have references.
        return false;
    }
    
    
    /**
     * A method for getting the references to other Objects that this node is dependent on.
     * @return Returns a String array of all of the DataBase keys for the Objects that this node is dependent on.
     */
    public String[] getDBRefs() {
        String[] names; // the array to store the return values
        
        // If this node has first-level references, use them as the starting data set in the array
        ArrayList<String> refs = new ArrayList<>();
        
        for (ValueNode node : nodes) {
            if (node instanceof ReferenceNode) {
                refs.add( ((ReferenceNode)node).getValue() );
            }
        }
        
        // Convert the ArrayList to an array
        names = new String[refs.size()];
        for (int index = 0; index < refs.size(); index++) {
            names[index] = refs.get(index);
        }
        
        // For each sub-node, add it's references to the list.
        for (ValueNode node : nodes) {
            if (node instanceof DataNode) {
                names = concat(names, ((DataNode)node).getDBRefs()); // concat is a method in util that concatenates two arrays
            }
        }
        
        return names;
    }
    
    
    /**
     * A method for learning if a node has references to database objects that do not exist in the provided database
     * @param db the ObjectDB to be checked for the references that this node requires
     * @return Returns true if the node has a reference that does not exist in the database, and false if it does not.
     */
    public boolean hasUnresolvedDBRefs(ObjectDB db) {
        
        // Get a list of references for this object
        String[] references = getDBRefs();
        
        // Check each one against the object lists for the provided database
        for (String reference : references) {
            if (!db.containsObjectKey(reference)) return true;
        }
        
        // If none of the references were invalid, then we do not have any unresolved references.
        return false;
    }
    
    
    /**
     * A method that corrects class name for PATH assignments.
     * @param db the ObjectDB to get PATH assignments from
     * @param className the className to correct for PATH assignments
     * @return Returns the class name string, corrected for PATH assignments in the database provided.
     */
    public static String adaptClassName(ObjectDB db, String className) {
        Logger logger = Logger.getDefaultLogger();
      
        // If we have a PATH mapping for this className, correct for it.
        if (db.containsPathKey(className)) {
            String oldName = className;
            className = db.getPath(className);
            logger.log("Updated class name from \"" + oldName + "\" to \"" + className + ".\"");
            return className;
        }
        
        // If it does not, then no correction needs to be performed.
        else {
            logger.log("Did not update class name \"" + className + ".\"");
            return className;
        }
    }
    
    
    /**
     * A method for building a object out of the data node using reflection.
     * @return Returns the object represented by this node.
     */
    public Object toObject() {
        
        logger.log("Building \"" + name + "\" into an Object of Class \"" + className + ".\"");
        logger.incIndent();
        
        // Get the database to get references from
        //   Should this be a parameter passed to this method?
        ObjectDB db = ObjectDB.getGlobalDB();
        
        ArrayList<Object> params = new ArrayList<>();  // Stores the parameters that we have so far, in order
        
        for (ValueNode node : nodes) {
            if (node instanceof PrimitiveNode) {
                PrimitiveNode prim = (PrimitiveNode)node;
                try {
                    // Get the class that we are going to instantiate the object from and the constructors available to us to do this
                    String cName = prim.getClassName();
                    cName = adaptClassName(ObjectDB.getGlobalDB(), cName);
                    Class<?> c = Class.forName(cName);
                    Constructor<?>[] allConstructors = c.getDeclaredConstructors();
                    
                    // Parse through the constructors for one that only requires a String (all primitive classes have one)
                    for (Constructor<?> cont : allConstructors) {
                        Class<?>[] paramTypes = cont.getParameterTypes();
                        
                        // When we find the constructor, instantiate the object with it and add it to the parameters list.
                        if (paramTypes.length == 1 && paramTypes[0] == String.class) {
                            Object obj = cont.newInstance(prim.getValue());
                            params.add(obj);
                        }
                    }
                }
                
                // Exception Handling - should be more elegant than this for release.
                catch (Exception e) {
                    logger.log(e);
                }
            }
            else if (node instanceof ReferenceNode) {
                String refName = ((ReferenceNode)node).getValue();
                params.add(db.getObject(refName));
            }
            else if (node instanceof DataNode) {
                Object o = ((DataNode)node).toObject();
                params.add(o);
            }
            else if (node instanceof ArrayNode) {
                Object o = ((ArrayNode)node).toObject();
                params.add(o);
            }
        }
        
        logger.log("For object " + name + " there are " + params.size() + " parameter(s).");
        
        // Obtain a Class object for the new object's class
        Class<?> classObject;
        try {
            try {
                classObject = Class.forName(adaptClassName(ObjectDB.getGlobalDB(), className));
            }
            catch (ClassNotFoundException ex) {
                
                // If the initial class name searced failed, obtain the class from the library classes
                logger.log("Attempt to load class \"" + className + "\" from standard classes has failed.");
                logger.log("Attempting to load class \"" + className + "\" from libraries.");
                classObject = db.getLibraryClass(className);
                
            }
        }
        catch(ClassNotFoundException ex) {
            logger.log(ex);
            logger.decIndent();
            return null;
        }
        
        // Identify the correct constructor
        Constructor<?> corCont = null;
        Constructor<?>[] constructors = classObject.getDeclaredConstructors();
        
        logger.log("For object " + name + " there are " + constructors.length + " constructor(s).");
        
        // If the parameters match, instantiate an object out of it.
        for(Constructor<?> cont : constructors) {
            if (paramsMatch(cont, params)) {
                corCont = cont;
                logger.log("Correct constructor found for object " + name + ": " + corCont.toString());
            }
        }
        
        
        // Instantiate the Object
        if (corCont != null) {
            try {
                logger.log("Creating instance of object.");
                Object o = corCont.newInstance(toArray(params));
                logger.decIndent();
                return o;
            }
            catch (InvocationTargetException ex) {
                logger.log(ex);
                logger.log(ex.getTargetException());
                logger.decIndent();
            }
            catch (Exception ex) {
                logger.log(ex);
                logger.decIndent();
            }
        }
        else {
            logger.log("No correct constructor found.");
            logger.decIndent();
        }
        
        // If something went wrong, return a null value.
        return null;
    }
    
    
    /**
     * Checks if the parameters for the provided constructor and ArrayList of parameters are compatible.  Order is important.
     * @param constructor the constructor
     * @param params the list of parameters
     * @return Returns true if the parameters and the constructor are compatible, false if they are not.
     */
    private boolean paramsMatch( Constructor<?> constructor, ArrayList<Object> params ) {
        
        // Get the class types of the constructor, in order.
        Class<?>[] paramTypes = constructor.getParameterTypes();
        
        // If the two lists are not of the same size, then they are not compatible.
        if (paramTypes.length != params.size()) {
            return false;
        }
        
        for (int index = 0; index < paramTypes.length; index++) {
            logger.log("ParamType: " + correctForPrimitive(paramTypes[index]));
            logger.log("ParamVal:  " + params.get(index));
            logger.log("Param:     " + params.get(index).getClass());
            
            // If the parameter types (after primitives are changed to their respective wrappers) do not match at
            //    any one point, then the constructor and the parameters are not compatible.
            if (!correctForPrimitive(paramTypes[index]).isInstance(params.get(index))) {
                return false;
            }
        }
        
        // If all of the parameters match, then the two are compatible
        return true;
    }
    
    
    /**
     * Corrects the class for primitives - primitives are converted to their respective wrapper classes.  If the provided class is not a primitive, then it will be returned unmodified.
     * @param c the class to be corrected
     * @return Returns the class as its wrapper if it is a primitive, or unmodified if it is not a primitive.
     */
    private Class<?> correctForPrimitive(Class<?> c) {
        if (c.equals(Double.TYPE)) {
            return Double.class;
        }
        else if (c.equals(Integer.TYPE)) {
            return Integer.class;
        }
        else if (c.equals(Byte.TYPE)) {
            return Byte.class;
        }
        else if (c.equals(Short.TYPE)) {
            return Short.class;
        }
        else if (c.equals(Long.TYPE)) {
            return Long.class;
        }
        else if (c.equals(Float.TYPE)) {
            return Float.class;
        }
        else if (c.equals(Boolean.TYPE)) {
            return Boolean.class;
        }
        else if (c.equals(Character.TYPE)) {
            return Character.class;
        }
        return c;
    }
    
    private String[] data; // The lines of data in the node
    private String name; // The name of the object represented by the node
    private String className; // The name of the class of the object represented by the node
    
    private List<ValueNode> nodes; // The sub-nodes of this one

    private Logger logger = Logger.getDefaultLogger(); // The logger that will allow us to record what is happening
    
}
